面包屑内容响应式设计
概述
头部面包屑组件在窗口缩放时,内容会被挤压换行导致布局异常。本节利用 VueUse 的 useResizeObserver 组合式函数监听元素高度变化,当高度超过阈值时自动裁剪面包屑项,实现自适应的响应式布局。
问题分析
正常状态(宽度充足):
首页 / 课程管理 / 内容管理 / 课程列表
异常状态(宽度不足,内容被挤成两行):
首页 / 课程管理 / 内容管理
/ 课程列表 ← 布局被破坏
text
核心思路:当面包屑容器高度从 14px 变为 28px(换行)时,从数组头部删除元素,恢复单行显示。
技术方案
基于 useResizeObserver 的响应式实现
VueUse 的 useResizeObserver 是对原生 ResizeObserver API 的组合式封装,返回一个 stop 函数用于停止观察:
<!-- BreadcrumbResponsive.vue -->
<template>
<el-breadcrumb ref="breadcrumbRef" separator="/">
<el-breadcrumb-item
v-for="(item, index) in breadcrumbData"
:key="index"
:to="item.to"
>
{{ item.title }}
</el-breadcrumb-item>
</el-breadcrumb>
</template>
<script setup lang="ts">
import { ref, reactive, onUnmounted } from 'vue'
import { useResizeObserver } from '@vueuse/core'
interface BreadcrumbItem {
title: string
to?: string
}
const props = defineProps<{
items: BreadcrumbItem[]
}>()
const breadcrumbRef = ref<HTMLElement>()
const breadcrumbData = reactive<BreadcrumbItem[]>([...props.items])
const { stop } = useResizeObserver(breadcrumbRef, (entries) => {
const entry = entries[0]
const { width, height } = entry.contentRect
// 正常单行高度为 14px,超过说明已换行
if (height > 14 && breadcrumbData.length > 1) {
// 从头部删除一个元素
breadcrumbData.shift()
}
})
onUnmounted(() => {
stop()
})
</script>
vue
带恢复逻辑的完整实现
窗口放大时,需要恢复被裁剪的面包屑项。使用 removedItems 缓存被删除的元素,当宽度恢复时按序还原:
<script setup lang="ts">
import { ref, reactive, onUnmounted } from 'vue'
import { useResizeObserver } from '@vueuse/core'
interface BreadcrumbItem {
title: string
to?: string
}
const props = defineProps<{
items: BreadcrumbItem[]
}>()
const breadcrumbRef = ref<HTMLElement>()
const breadcrumbData = reactive<BreadcrumbItem[]>([...props.items])
const removedItems = reactive<BreadcrumbItem[]>([])
const { stop } = useResizeObserver(breadcrumbRef, (entries) => {
const height = entries[0].contentRect.height
if (height > 14 && breadcrumbData.length > 1) {
// 收窄:从头部移除,压入缓存
const removed = breadcrumbData.shift()!
removedItems.push(removed)
} else if (height <= 14 && removedItems.length > 0) {
// 恢复:从缓存中还原到头部
const restored = removedItems.pop()!
breadcrumbData.unshift(restored)
}
})
onUnmounted(() => stop())
</script>
vue
useResizeObserver API 详解
import { useResizeObserver } from '@vueuse/core'
const { stop } = useResizeObserver(
target, // Ref<HTMLElement | null> — 要观察的 DOM 元素
(entries) => { // ResizeObserverCallback — 尺寸变化时的回调
const entry = entries[0]
const { width, height } = entry.contentRect
// 或者使用 contentBoxSize(更精确)
const { inlineSize, blockSize } = entry.contentBoxSize[0]
}
)
// 停止观察
stop()
ts
| 属性/方法 | 说明 |
|---|---|
entries[0].contentRect.height | 内容区域高度 |
entries[0].contentRect.width | 内容区域宽度 |
entries[0].contentBoxSize[0] | 更精确的尺寸信息(inlineSize/blockSize) |
entries[0].target | 被观察的目标元素 |
useResizeObserver().stop() | 停止观察(来自 VueUse) |
支持多元素同时观察:
const els = [ref<HTMLElement>(), ref<HTMLElement>()]
useResizeObserver(els, (entries) => {
entries.forEach(entry => {
console.log(entry.target, entry.contentRect)
})
})
ts
关键实现细节
高度阈值判断
单行高度:14px(el-breadcrumb 默认行高)
双行高度:28px
判断条件:height > 14 即说明发生了换行
text
抖动问题处理
当面包屑只剩两项时,删除一项后高度恢复正常,触发恢复逻辑又加回来,导致循环抖动。解决方案:
// 设置最小保留项数,避免抖动
const MIN_ITEMS = 2
if (height > 14 && breadcrumbData.length > MIN_ITEMS) {
const removed = breadcrumbData.shift()!
removedItems.push(removed)
}
ts
shift() 方法说明
shift() 从数组头部删除元素并返回被删除的元素,与 pop()(从尾部删除)相对应。在面包屑场景中,优先删除最左侧(最早)的项,保留离当前页面最近的面包屑路径。
方案对比
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| ResizeObserver | 监听元素尺寸变化 | 性能好、原生 API | 需手动管理删除/恢复 |
| CSS text-overflow | text-overflow: ellipsis | 纯 CSS、简单 | 只能截断文本,不能减少项 |
| CSS flex-shrink | flex-shrink: 0 + overflow: hidden | 纯 CSS | 无法智能裁剪 |
| 媒体查询 | @media (max-width: 768px) | 预定义断点 | 无法精确响应内容变化 |
实践要点
useResizeObserver来自 VueUse,是对原生ResizeObserver的组合式封装,自动处理生命周期shift()方法从数组头部删除元素并返回被删除的元素- 需要在组件卸载时调用
stop()停止观察,避免内存泄漏 - 设置最小保留项数(如 2 项)可有效避免循环抖动
- 使用
contentBoxSize可以获取更精确的尺寸信息,适用于高 DPI 屏幕
↑